/*
 * Copyright 2014 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.javascript.jscomp;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.DUPLICATE_MODULE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.DUPLICATE_NAMESPACE_AND_MODULE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_FORWARD_DECLARE_NAMESPACE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_GET_NAMESPACE;
import static com.google.javascript.jscomp.ClosureRewriteModule.ILLEGAL_MODULE_RENAMING_CONFLICT;
import static com.google.javascript.jscomp.ClosureRewriteModule.ILLEGAL_STMT_OF_GOOG_REQUIRE_DYNAMIC_IN_AWAIT;
import static com.google.javascript.jscomp.ClosureRewriteModule.IMPORT_INLINING_SHADOWS_VAR;
import static com.google.javascript.jscomp.ClosureRewriteModule.INVALID_EXPORT_COMPUTED_PROPERTY;
import static com.google.javascript.jscomp.ClosureRewriteModule.LOAD_MODULE_FN_MISSING_RETURN;
import static com.google.javascript.jscomp.modules.ModuleMapCreator.DOES_NOT_HAVE_EXPORT_WITH_DETAILS;
import static com.google.javascript.rhino.testing.NodeSubject.assertNode;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Predicates;
import com.google.javascript.jscomp.testing.TestExternsBuilder;
import com.google.javascript.jscomp.type.ReverseAbstractInterpreter;
import com.google.javascript.jscomp.type.SemanticReverseAbstractInterpreter;
import com.google.javascript.rhino.Node;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for ClosureRewriteModule */
@RunWith(JUnit4.class)
public final class ClosureRewriteModuleTest extends CompilerTestCase {

  private boolean preserveClosurePrimitives = false;

  public ClosureRewriteModuleTest() {
    super(new TestExternsBuilder().addClosureExterns().addPromise().addConsole().build());
  }

  @Override
  protected CompilerPass getProcessor(Compiler compiler) {
    return (externs, main) -> {
      ReverseAbstractInterpreter rai =
          new SemanticReverseAbstractInterpreter(compiler.getTypeRegistry());
      TypedScope globalTypedScope =
          checkNotNull(
              new TypeCheck(compiler, rai, compiler.getTypeRegistry())
                  .processForTesting(externs, main));
      compiler.setTypeCheckingHasRun(true);

      new ClosureRewriteModule(compiler, null, globalTypedScope).process(externs, main);
    };
  }

  @Override
  @Before
  public void setUp() throws Exception {
    super.setUp();
    preserveClosurePrimitives = false;
    enableCreateModuleMap();
    enableTypeInfoValidation();
  }

  @Override
  protected CompilerOptions getOptions() {
    CompilerOptions options = super.getOptions();
    options.setWarningLevel(DiagnosticGroups.LINT_CHECKS, CheckLevel.WARNING);
    options.setPreserveClosurePrimitives(this.preserveClosurePrimitives);
    options.setWarningLevel(DiagnosticGroups.MISSING_PROVIDE, CheckLevel.WARNING);
    options.setWarningLevel(DiagnosticGroups.MODULE_LOAD, CheckLevel.OFF);
    options.setPrettyPrint(true);
    return options;
  }

  @Test
  public void testEmpty() {
    testSame("");
  }

  @Test
  public void testProvide() {
    testSame("goog.provide('a');");
  }

  @Test
  public void testModule() {
    test("goog.module('a');", "/** @const */ var module$exports$a = {};");
  }

  @Test
  public void testRequireModule() {
    test(
        srcs(
            "goog.module('ns.b');",
            "goog.module('ns.c');",
            lines(
                "goog.module('ns.a');",
                "var b = goog.require('ns.b');",
                "var c = goog.requireType('ns.c');")),
        expected(
            "/** @const */ var module$exports$ns$b = {};",
            "/** @const */ var module$exports$ns$c = {};",
            "/** @const */ var module$exports$ns$a = {};"));
  }

  @Test
  public void testOutOfOrderRequireType() {
    test(
        srcs(
            lines(
                "goog.module('ns.a');",
                "var b = goog.requireType('ns.b');",
                "/** @type {b.Foo} */",
                "let a;",
                ""),
            lines("goog.module('ns.b');", "exports.Foo = class {}", "")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$a = {};",
                "/** @type {module$exports$ns$b.Foo} */ let module$contents$ns$a_a;",
                ""),
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const */ module$exports$ns$b.Foo = class {};",
                "")));
  }

  @Test
  public void testRequireModuleMultivar() {
    test(
        srcs(
            "goog.module('ns.b');",
            "goog.module('ns.c');",
            lines(
                "goog.module('ns.a');",
                "var b = goog.require('ns.b'), c = goog.requireType('ns.c');")),
        expected(
            "/** @const */ var module$exports$ns$b = {};",
            "/** @const */ var module$exports$ns$c = {};",
            "/** @const */ var module$exports$ns$a = {};"));
  }

  @Test
  public void testIjsModule() {
    allowExternsChanges();
    test(
        srcs(
            // .i.js files
            lines(
                "/** @typeSummary */",
                "goog.module('external1');",
                "/** @constructor */",
                "exports = function() {};"),
            lines(
                "/** @typeSummary */",
                "goog.module('external2');",
                "/** @constructor */",
                "exports = function() {};"),
            // source file
            lines(
                "goog.module('ns.a');",
                "var b = goog.require('external1');",
                "var c = goog.requireType('external2');",
                "/** @type {b} */ new b;",
                "/** @typedef {c} */ var d;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$a = {};",
                "/** @type {module$exports$external1} */ new module$exports$external1",
                "/** @typedef {module$exports$external2} */ var module$contents$ns$a_d")));
  }

  @Test
  public void testDestructuringInsideModule() {
    // Array destrucuturing
    test(
        lines("goog.module('a');", "var [x, y, z] = foo();"),
        lines(
            "/** @const */ var module$exports$a = {};",
            "var [module$contents$a_x, module$contents$a_y, module$contents$a_z] = foo();"));

    ignoreWarnings(TypeCheck.POSSIBLE_INEXISTENT_PROPERTY);

    // Object destructuring with explicit names
    test(
        lines("goog.module('a');", "var {p1: x, p2: y} = foo();"),
        lines(
            "/** @const */ var module$exports$a = {};",
            "var {p1: module$contents$a_x, p2: module$contents$a_y} = foo();"));

    // Object destructuring with short names
    test(
        lines("goog.module('a');", "var {x, y} = foo();"),
        lines(
            "/** @const */ var module$exports$a = {};",
            "var {x: module$contents$a_x, y: module$contents$a_y} = foo();"));
  }

  @Test
  public void testShortObjectLiteralsInsideModule() {
    test(
        lines("goog.module('a');", "var x = foo();", "var o = {x};"),
        lines(
            "/** @const */ var module$exports$a = {};",
            "var module$contents$a_x = foo();",
            "var module$contents$a_o = {x: module$contents$a_x};"));

    test(
        lines("goog.module('a');", "var x = foo();", "exports = {x};"),
        lines("/** @const */ var module$exports$a = {};", "module$exports$a.x = foo();"));
  }

  @Test
  public void testNamedExportInliningJSDoc_atConstJSDoc() {
    test(
        srcs(
            lines(
                "goog.module('ns.b');", //
                "/** @const */ var Foo = {a: 0};",
                "exports.Foo = Foo;"),
            lines(
                "goog.module('ns.a');", //
                "var {Foo} = goog.require('ns.b');",
                "var f = Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const */ module$exports$ns$b.Foo = {a: 0};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "var module$contents$ns$a_f = module$exports$ns$b.Foo;")));
  }

  @Test
  public void testNamedExportInliningJSDoc_semanticConst() {
    test(
        srcs(
            lines(
                "goog.module('ns.b');", //
                "const Foo = {a: 0};",
                "exports.Foo = Foo;"),
            lines(
                "goog.module('ns.a');", //
                "var {Foo} = goog.require('ns.b');",
                "var f = Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const */ module$exports$ns$b.Foo = {a: 0};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "var module$contents$ns$a_f = module$exports$ns$b.Foo;")));
  }

  @Test
  public void testNamedExportInliningJSDoc_semanticConstWithExistingJSDoc() {
    test(
        srcs(
            lines(
                "goog.module('ns.b');", //
                "/** @interface */ const Foo = class {};",
                "exports.Foo = Foo;"),
            lines(
                "goog.module('ns.a');", //
                "var {Foo} = goog.require('ns.b');",
                "var f = Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const @interface */ module$exports$ns$b.Foo = class {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "var module$contents$ns$a_f = module$exports$ns$b.Foo;")));
  }

  @Test
  public void testNamedExportInliningJSDoc_semanticConstWithInlineJSDoc() {
    test(
        srcs(
            lines(
                "goog.module('ns.b');", //
                "const /** number */ foo = 0;",
                "exports.foo = foo;"),
            lines(
                "goog.module('ns.a');", //
                "var {foo} = goog.require('ns.b');",
                "var f = foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const {number} */ module$exports$ns$b.foo = 0;"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "var module$contents$ns$a_f = module$exports$ns$b.foo;")));
  }

  @Test
  public void testNamedExportInliningJSDoc_nonConstWithInlineJSDoc() {
    test(
        srcs(
            lines(
                "goog.module('ns.b');", //
                "let /** number */ foo = 0;",
                "exports.foo = foo;"),
            lines(
                "goog.module('ns.a');", //
                "var {foo} = goog.require('ns.b');",
                "var f = foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @type {number} */ module$exports$ns$b.foo = 0;"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "var module$contents$ns$a_f = module$exports$ns$b.foo;")));
  }

  @Test
  public void testNamedExportInliningJSDoc_mutatedLocal() {
    // TODO(lharker): Stop inlining 'exports.Foo -> Foo' in this case.
    test(
        srcs(
            lines(
                "goog.module('ns.b');", //
                "let Foo = {a: 0};",
                "exports.Foo = Foo;",
                "Foo = {};"),
            lines(
                "goog.module('ns.a');", //
                "var {Foo} = goog.require('ns.b');",
                "var f = Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "module$exports$ns$b.Foo = {a: 0};",
                "module$exports$ns$b.Foo = {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "var module$contents$ns$a_f = module$exports$ns$b.Foo;")));
  }

  @Test
  public void testDeepNamespace() {
    test(
        srcs(
            "goog.provide('ns.a.b.Foo'); /** @constructor */ ns.a.b.Foo = function() {};",
            lines(
                "goog.module('ns.other');",
                "",
                "var Foo = goog.require('ns.a.b.Foo');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            "goog.provide('ns.a.b.Foo'); /** @constructor */ ns.a.b.Foo = function() {};",
            lines(
                "/** @const */ var module$exports$ns$other = {}",
                "goog.require('ns.a.b.Foo');",
                "/** @type {ns.a.b.Foo} */",
                "var module$contents$ns$other_f = new ns.a.b.Foo;")));
  }

  @Test
  public void testDestructuringImports() {
    test(
        srcs(
            "goog.module('ns.b'); /** @constructor */ exports.Foo = function() {};",
            lines(
                "goog.module('ns.a');",
                "",
                "var {Foo} = goog.require('ns.b');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @constructor @const */ module$exports$ns$b.Foo = function() {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {module$exports$ns$b.Foo} */",
                "var module$contents$ns$a_f = new module$exports$ns$b.Foo;")));

    test(
        srcs(
            "goog.module('ns.b'); /** @typedef {number} */ exports.Foo;",
            lines(
                "goog.module('ns.a');",
                "",
                "var {Foo} = goog.require('ns.b');",
                "",
                "/** @type {Foo} */",
                "var f = 4;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const @typedef {number} */ module$exports$ns$b.Foo;"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {module$exports$ns$b.Foo} */",
                "var module$contents$ns$a_f = 4;")));

    test(
        srcs(
            "goog.provide('ns.b'); /** @constructor */ ns.b.Foo = function() {};",
            lines(
                "goog.module('ns.a');",
                "",
                "var {Foo} = goog.require('ns.b');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            "goog.provide('ns.b'); /** @constructor */ ns.b.Foo = function() {};",
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {ns.b.Foo} */",
                "var module$contents$ns$a_f = new ns.b.Foo;")));

    test(
        srcs(
            lines(
                "goog.module('ns.b');",
                "goog.module.declareLegacyNamespace();",
                "",
                "/** @constructor */ exports.Foo = function() {};"),
            lines(
                "goog.module('ns.a');",
                "",
                "var {Foo} = goog.require('ns.b');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            lines("goog.provide('ns.b');", "/** @constructor @const */ ns.b.Foo = function() {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {ns.b.Foo} */",
                "var module$contents$ns$a_f = new ns.b.Foo;")));

    test(
        srcs(
            "goog.module('ns.b'); /** @constructor */ exports.Foo = function() {};",
            lines(
                "goog.module('ns.a');",
                "",
                "var {Foo: Bar} = goog.require('ns.b');",
                "",
                "/** @type {Bar} */",
                "var f = new Bar;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @constructor @const */ module$exports$ns$b.Foo = function() {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {module$exports$ns$b.Foo} */",
                "var module$contents$ns$a_f = new module$exports$ns$b.Foo;")));

    test(
        srcs(
            "goog.module('modA'); class Foo {} exports.Foo = Foo;",
            lines(
                "goog.module('modB');",
                "",
                "var {Foo:importedFoo} = goog.require('modA');",
                "",
                "/** @type {importedFoo} */",
                "var f = new importedFoo;")),
        expected(
            lines(
                "/** @const */ var module$exports$modA = {};",
                "module$exports$modA.Foo = class {}"),
            lines(
                "/** @const */ var module$exports$modB = {}",
                "/** @type {module$exports$modA.Foo} */",
                "var module$contents$modB_f = new module$exports$modA.Foo;")));

    test(
        srcs(
            "goog.module('modA'); class Foo {} exports.Foo = Foo;",
            lines(
                "goog.module('modB');",
                "",
                "var {Foo} = goog.require('modA');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$modA = {};",
                "module$exports$modA.Foo = class {}"),
            lines(
                "/** @const */ var module$exports$modB = {}",
                "/** @type {module$exports$modA.Foo} */",
                "var module$contents$modB_f = new module$exports$modA.Foo;")));

    test(
        srcs(
            "goog.module('modA'); class Foo {} exports = {Foo};",
            lines(
                "goog.module('modB');",
                "",
                "var {Foo} = goog.require('modA');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$modA = {};",
                "module$exports$modA.Foo = class {};"),
            lines(
                "/** @const */ var module$exports$modB = {}",
                "/** @type {module$exports$modA.Foo} */",
                "var module$contents$modB_f = new module$exports$modA.Foo;")));

    test(
        srcs(
            "goog.module('modA'); class Bar {} exports = class Foo {}; exports.Bar = Bar;",
            lines(
                "goog.module('modB');",
                "",
                "var Foo = goog.require('modA');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            lines(
                "class module$contents$modA_Bar {}",
                "/** @const */ var module$exports$modA = class Foo {};",
                "/** @const */ module$exports$modA.Bar = module$contents$modA_Bar;"),
            lines(
                "/** @const */ var module$exports$modB = {}",
                "/** @type {module$exports$modA} */",
                "var module$contents$modB_f = new module$exports$modA;")));

    test(
        srcs(
            lines(
                "goog.module('modA');",
                "goog.module.declareLegacyNamespace();",
                "",
                "class Foo {}",
                "exports = {Foo};"),
            lines(
                "goog.module('modB');",
                "",
                "var {Foo} = goog.require('modA');",
                "",
                "/** @type {Foo} */",
                "var f = new Foo;")),
        expected(
            lines(
                "goog.provide('modA');",
                "class module$contents$modA_Foo {}",
                "/** @const */ modA.Foo = module$contents$modA_Foo;"),
            lines(
                "/** @const */ var module$exports$modB = {}",
                "/** @type {modA.Foo} */",
                "var module$contents$modB_f = new modA.Foo;")));
  }

  @Test
  public void testUninlinableExports() {
    test(
        srcs(
            "goog.module('ns.b'); /** @constructor */ exports.Foo = function() {};",
            lines(
                "goog.module('ns.a');",
                "var {Foo} = goog.require('ns.b');",
                "/** @type {Foo} */ var f = new Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const @constructor */ module$exports$ns$b.Foo = function() {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {module$exports$ns$b.Foo} */",
                "var module$contents$ns$a_f = new module$exports$ns$b.Foo;")));

    test(
        srcs(
            "goog.module('ns.b'); /** @constructor */ exports.Foo = function() {};",
            lines(
                "goog.module('ns.a');",
                "var {Foo} = goog.requireType('ns.b');",
                "/** @type {Foo} */ var f;")),
        expected(
            lines(
                "/** @const */ var module$exports$ns$b = {};",
                "/** @const @constructor */ module$exports$ns$b.Foo = function() {};"),
            lines(
                "/** @const */ var module$exports$ns$a = {}",
                "/** @type {module$exports$ns$b.Foo} */",
                "var module$contents$ns$a_f;")));
  }

  @Test
  public void testObjectLiteralDefaultExport() {
    testError(
        srcs(
            lines(
                "goog.module('modA');",
                "",
                "class Foo {}",
                "// This is not a named exports object because of the value literal",
                "exports = {Foo, Bar: [1,2,3]};"),
            lines("goog.module('modB');", "", "var {Foo} = goog.require('modA');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    testError(
        srcs(
            lines(
                "goog.module('modA');",
                "",
                "class Foo {}",
                "// This is not a named exports object because of the value literal",
                "exports = {Foo, Bar: [1,2,3]};"),
            lines("goog.module('modB');", "", "var {Foo} = goog.requireType('modA');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);
  }

  @Test
  public void testDontUseTheMangledModuleNameInCode() {
    //
    testError(
        srcs(
            "/** @const */ var module$exports$modA = class {};", //
            "goog.module('modA');"),
        ILLEGAL_MODULE_RENAMING_CONFLICT);
  }

  @Test
  public void testUninlinableNamedExports() {
    test(
        srcs(
            "goog.module('modA'); \n exports = class {};",
            lines(
                "goog.module('modB');",
                "",
                "var Foo = goog.require('modA');",
                "",
                "exports.Foo = Foo;")),
        expected(
            "/** @const */ var module$exports$modA = class {};",
            lines(
                "/** @const */ var module$exports$modB = {};",
                "/** @const */ module$exports$modB.Foo = module$exports$modA;")));

    test(
        srcs(
            "goog.module('modA'); \n exports = class {};",
            lines(
                "goog.module('modB');",
                "",
                "var Foo = goog.require('modA');",
                "",
                "exports = {Foo};")),
        expected(
            "/** @const */ var module$exports$modA = class {};",
            lines(
                "/** @const */ var module$exports$modB = {};",
                "/** @const */ module$exports$modB.Foo = module$exports$modA;")));

    test(
        srcs(
            "goog.module('modA'); \n exports = class {};",
            lines(
                "goog.module('modB');",
                "",
                "var Foo = goog.require('modA');",
                "",
                "exports = {ExportedFoo: Foo};")),
        expected(
            "/** @const */ var module$exports$modA = class {};",
            lines(
                "/** @const */ var module$exports$modB = {};",
                "/** @const */ module$exports$modB.ExportedFoo = module$exports$modA;")));

    test(
        srcs(
            "goog.module('modA'); \n exports = class {};",
            lines(
                "goog.module('modB');",
                "",
                "var Foo = goog.require('modA');",
                "class Bar {}",
                "",
                "exports = {Foo, Bar};")),
        expected(
            "/** @const */ var module$exports$modA = class {};",
            lines(
                "/** @const */ var module$exports$modB = {};",
                "module$exports$modB.Bar = class {};",
                "/** @const */ module$exports$modB.Foo = module$exports$modA;")));
  }

  @Test
  public void testIllegalDestructuringImports() {
    testError(
        srcs(
            lines(
                "goog.module('p.A');",
                "/** @constructor */ var A = function() {}",
                "A.method = function() {}",
                "exports = A"),
            lines("goog.module('p.C');", "var {method} = goog.require('p.A');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    testError(
        srcs(
            lines(
                "goog.module('p.A');",
                "/** @constructor */ var A = function() {}",
                "A.method = function() {}",
                "exports = A"),
            lines("goog.module('p.C');", "var {method} = goog.requireType('p.A');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    testError(
        srcs(
            lines(
                "goog.module('p.A');",
                "/** @constructor */ exports = class { static method() {} }"),
            lines("goog.module('p.C');", "var {method} = goog.require('p.A');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    testError(
        srcs(
            lines(
                "goog.module('p.A');",
                "/** @constructor */ exports = class { static method() {} }"),
            lines("goog.module('p.C');", "var {method} = goog.requireType('p.A');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    testError(
        srcs(
            lines(
                "goog.module('p.A');",
                "",
                "/** @constructor */ exports.Foo = class {};",
                "/** @constructor */ exports.Bar = class {};"),
            lines("goog.module('p.C');", "", "var {Baz} = goog.require('p.A');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    testError(
        srcs(
            lines(
                "goog.module('p.A');",
                "",
                "/** @constructor */ exports.Foo = class {};",
                "/** @constructor */ exports.Bar = class {};"),
            lines("goog.module('p.C');", "", "var {Baz} = goog.requireType('p.A');")),
        DOES_NOT_HAVE_EXPORT_WITH_DETAILS);

    // TODO(blickly): We should warn for the next two as well, but it's harder to detect.

    test(
        srcs(
            lines(
                "goog.provide('p.A');",
                "/** @constructor */ p.A = function() {}",
                "p.A.method = function() {}"),
            lines("goog.module('p.C');", "var {method} = goog.require('p.A');")),
        expected((String[]) null));

    test(
        srcs(
            lines(
                "goog.provide('p.A');",
                "/** @constructor */ p.A = function() {}",
                "p.A.method = function() {}"),
            lines("goog.module('p.C');", "var {method} = goog.requireType('p.A');")),
        expected((String[]) null));
  }

  @Test
  public void testDeclareLegacyNamespace() {
    test("goog.module('ns.a'); goog.module.declareLegacyNamespace();", "goog.provide('ns.a');");
  }

  @Test
  public void testSideEffectOnlyModuleImport() {
    test(
        srcs(
            lines("goog.module('ns.b');", "alert('hello world');"),
            lines("goog.module('ns.c');", "alert('hello world');"),
            lines("goog.module('ns.a');", "goog.require('ns.b');", "goog.requireType('ns.c');")),
        expected(
            "/** @const */ var module$exports$ns$b = {}; alert('hello world');",
            "/** @const */ var module$exports$ns$c = {}; alert('hello world');",
            "/** @const */ var module$exports$ns$a = {};"));
  }

  @Test
  public void testTypeOnlyModuleImport() {
    test(
        srcs(
            lines("goog.module('ns.B');", "/** @constructor */ exports = function() {};"),
            lines("goog.module('ns.C');", "/** @constructor */ exports = function() {};"),
            lines(
                "goog.module('ns.a');",
                "goog.require('ns.B');",
                "goog.requireType('ns.C');",
                "/** @type {ns.B} */ var b;",
                "/** @type {ns.C} */ var c;")),
        expected(
            "/** @constructor @const */ var module$exports$ns$B = function() {};",
            "/** @constructor @const */ var module$exports$ns$C = function() {};",
            lines(
                "/** @const */ var module$exports$ns$a = {};",
                "/** @type {module$exports$ns$B} */ var module$contents$ns$a_b;",
                "/** @type {module$exports$ns$C} */ var module$contents$ns$a_c;")));
  }

  @Test
  public void testSideEffectOnlyImportOfGoogProvide() {
    test(
        srcs(
            lines("goog.provide('ns.b');", "alert('hello world');"),
            lines("goog.provide('ns.c');", "alert('hello world');"),
            lines("goog.module('ns.a');", "goog.require('ns.b');", "goog.requireType('ns.c');")),
        expected(
            "goog.provide('ns.b'); alert('hello world');",
            "goog.provide('ns.c'); alert('hello world');",
            lines(
                "/** @const */ var module$exports$ns$a = {};",
                "goog.require('ns.b');",
                "goog.requireType('ns.c');")));
  }

  @Test
  public void testSideEffectOnlyImportOfLegacyGoogModule() {
    test(
        srcs(
            lines(
                "goog.module('ns.b');",
                "goog.module.declareLegacyNamespace();",
                "alert('hello world');"),
            lines(
                "goog.module('ns.c');",
                "goog.module.declareLegacyNamespace();",
                "alert('hello world');"),
            lines("goog.module('ns.a');", "goog.require('ns.b');", "goog.requireType('ns.c');")),
        expected(
            "goog.provide('ns.b'); alert('hello world');",
            "goog.provide('ns.c'); alert('hello world');",
            lines(
                "/** @const */ var module$exports$ns$a = {};",
                "goog.require('ns.b');",
                "goog.requireType('ns.c');")));
  }

  @Test
  public void testTypeOnlyModuleImportFromLegacyFile() {
    test(
        srcs(
            lines("goog.module('ns.B');", "/** @constructor */ exports = function() {};"),
            lines("goog.module('ns.C');", "/** @constructor */ exports = function() {};"),
            lines(
                "goog.provide('ns.a');",
                "goog.require('ns.B');",
                "goog.requireType('ns.C');",
                "/** @type {ns.B} */ var b;",
                "/** @type {ns.C} */ var c;")),
        expected(
            "/** @constructor @const */ var module$exports$ns$B = function() {};",
            "/** @constructor @const */ var module$exports$ns$C = function() {};",
            lines(
                "goog.provide('ns.a');",
                "/** @type {module$exports$ns$B} */ var b;",
                "/** @type {module$exports$ns$C} */ var c;")));
  }

  @Test
  public void testTypeOnlyModuleImportFromLegacyFile_ofLegacyModule() {
    test(
        srcs(
            lines(
                "goog.module('ns.C');",
                "goog.module.declareLegacyNamespace();",
                "/** @constructor */ exports = function() {};"),
            lines(
                "goog.provide('ns.a');",
                "goog.requireType('ns.C');",
                "/** @type {ns.C} */ var c;")),
        expected(
            lines(
                "goog.provide('ns.C');", //
                "/** @constructor @const */ ns.C = function() {};"),
            lines(
                "goog.provide('ns.a');", //
                "goog.requireType('ns.C');",
                "/** @type {ns.C} */ var c;")));
  }

  @Test
  public void testBundle1() {
    test(
        srcs(
            "goog.module('ns.b');",
            lines(
                "goog.loadModule(function(exports) {",
                "  goog.module('ns.a');",
                "  var b = goog.require('ns.b');",
                "  exports.b = b;",
                "  return exports;",
                "});")),
        expected(
            "/** @const */ var module$exports$ns$b = {};",
            lines(
                "/** @const */ var module$exports$ns$a = {};",
                "/** @const */ module$exports$ns$a.b = module$exports$ns$b;")));
  }

  @Test
  public void testBundle2() {
    test(
        lines(
            "goog.loadModule(function(exports) {",
            "  goog.module('ns.b');",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) {",
            "  goog.module('ns.a');",
            "  var b = goog.require('ns.b');",
            "  exports.b = b;",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) {",
            "  goog.module('ns.c');",
            "  var b = goog.require('ns.b');",
            "  exports.b = b;",
            "  return exports;",
            "});"),
        lines(
            "/** @const */ var module$exports$ns$b = {};",
            "/** @const */ var module$exports$ns$a = {};",
            "/** @const */ module$exports$ns$a.b = module$exports$ns$b;",
            "/** @const */ var module$exports$ns$c = {};",
            "/** @const */ module$exports$ns$c.b = module$exports$ns$b;"));
  }

  @Test
  public void testBundle3() {
    test(
        lines(
            "goog.loadModule(function(exports) {",
            "  goog.module('ns.b');",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) {",
            "  'use strict';",
            "  goog.module('ns.a');",
            "  goog.module.declareLegacyNamespace();",
            "  var b = goog.require('ns.b');",
            "  return exports;",
            "});"),
        lines("/** @const */ var module$exports$ns$b = {};", "goog.provide('ns.a');"));
  }

  @Test
  public void testBundle4() {
    test(
        lines(
            "goog.loadModule(function(exports) {",
            "  goog.module('goog.asserts');",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) {",
            "  'use strict';",
            "  goog.module('ns.a');",
            "  var b = goog.require('goog.asserts');",
            "  return exports;",
            "});"),
        lines(
            "/** @const */ var module$exports$goog$asserts = {};",
            "/** @const */ var module$exports$ns$a = {};"));
  }

  @Test
  public void testBundle5() {
    test(
        lines(
            "goog.loadModule(function(exports) {",
            "  goog.module('goog.asserts');",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) {",
            "  'use strict';",
            "  goog.module('xid');",
            "  goog.module.declareLegacyNamespace();",
            "  var asserts = goog.require('goog.asserts');",
            "  exports = function(id) {",
            "    return xid.internal_(id);",
            "  };",
            "  var xid = exports;",
            "  exports.internal_ = function(id) {};",
            "  return exports;",
            "});"),
        lines(
            "/** @const */ var module$exports$goog$asserts = {};",
            "goog.provide('xid');",
            "/** @const */ xid = function(id) {",
            "  return module$contents$xid_xid.internal_(id);",
            "};",
            "var module$contents$xid_xid = xid;",
            "/** @const */",
            "module$contents$xid_xid.internal_ = function(id) {};"));
  }

  @Test
  public void testBundle6() {
    test(
        lines(
            "goog.loadModule(function(exports) {",
            "  goog.module('goog.asserts');",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) {",
            "  'use strict';",
            "  goog.module('xid');",
            "  goog.module.declareLegacyNamespace();",
            "  var asserts = goog.require('goog.asserts');",
            "  var xid = function(id) {",
            "    return xid.internal_(id);",
            "  };",
            "  xid.internal_ = function(id) {};",
            "  exports = xid;",
            "  return exports;",
            "});"),
        lines(
            "/** @const */ var module$exports$goog$asserts = {};",
            "goog.provide('xid');",
            "var module$contents$xid_xid = function(id) {",
            "  return module$contents$xid_xid.internal_(id);",
            "};",
            "module$contents$xid_xid.internal_ = function(id) {};",
            "/** @const */ xid = module$contents$xid_xid "));
  }

  @Test
  public void testBundleWithDestructuringImport() {
    test(
        lines(
            "goog.loadModule(function(exports) { 'use strict';",
            "  goog.module('mod_B');",
            "",
            "  /** @interface */ function B(){}",
            "",
            "  exports.B = B;",
            "  return exports;",
            "});",
            "goog.loadModule(function(exports) { 'use strict';",
            "  goog.module('mod_A');",
            "",
            "  var {B} = goog.require('mod_B');",
            "",
            "  /** @constructor @implements {B} */",
            "  function A() {}",
            "  return exports;",
            "});"),
        lines(
            "/** @const */ var module$exports$mod_B = {};",
            "/** @interface */ function module$contents$mod_B_B(){}",
            "/** @const */ module$exports$mod_B.B = module$contents$mod_B_B;",
            "",
            "/** @const */ var module$exports$mod_A = {};",
            "/** @constructor @implements {module$exports$mod_B.B} */",
            "function module$contents$mod_A_A(){}"));
  }

  @Test
  public void testGoogLoadModule_missingReturn() {
    testError(
        lines(
            "goog.loadModule(function(exports) {", //
            "  goog.module('ns.b');",
            "});"),
        LOAD_MODULE_FN_MISSING_RETURN);
  }

  @Test
  public void testGoogLoadModuleString() {
    assertThrows(
        IllegalArgumentException.class,
        () -> testSame("goog.loadModule(\"goog.module('a.b.c'); exports = class {};\");"));
  }

  @Test
  public void testTopLevelNames1() {
    // Vars defined inside functions are not module top level.
    test(
        lines(
            "goog.module('a');",
            "var a, b, c;",
            "function Outer() {",
            "  var a, b, c;",
            "  function Inner() {",
            "    var a, b, c;",
            "  }",
            "}"),
        lines(
            "/** @const */ var module$exports$a = {};",
            "var module$contents$a_a, module$contents$a_b, module$contents$a_c;",
            "function module$contents$a_Outer() {",
            "  var a, b, c;",
            "  function Inner() {",
            "    var a, b, c",
            "  }",
            "}"));
  }

  @Test
  public void testTopLevelNames2() {
    // Vars in blocks are module top level because they are hoisted to the first execution context.
    test(
        lines("goog.module('a.c');", "if (true) {", "  var a, b, c;", "}"),
        lines(
            "/** @const */ var module$exports$a$c = {};",
            "if (true) {",
            "  var module$contents$a$c_a, module$contents$a$c_b, module$contents$a$c_c;",
            "}"));
  }

  @Test
  public void testTopLevelNames3() {
    // Functions in blocks are not module top level because they are block scoped.
    test(
        lines(
            "goog.module('a.c');",
            "if (true) {",
            "  function a() {}",
            "  function b() {}",
            "  function c() {}",
            "}"),
        lines(
            "/** @const */ var module$exports$a$c = {};",
            "if (true) {",
            "  function a() {}",
            "  function b() {}",
            "  function c() {}",
            "}"));
  }

  @Test
  public void testThis() {
    // global "this" is retained.
    test(
        lines("goog.module('a');", "this;"),
        lines("/** @const */ var module$exports$a = {};", "this;"));
  }

  @Test
  public void testGoogImport_await() {
    test(
        srcs(
            "goog.module('a.b.c');",
            lines("async function test() {", " var d = await goog.requireDynamic('a.b.c');", "}")),
        expected(
            "/** @const */ var module$exports$a$b$c = {}",
            lines(
                "async function test() {",
                " await goog.importHandler_('sG5M4c');",
                " var d = module$exports$a$b$c;",
                "}")));
  }

  @Test
  public void testGoogImport_await_destructuring() {
    test(
        srcs(
            lines("goog.module('a.b.c');", "exports.Foo=class{}"),
            lines(
                "async function test() {",
                " var {Foo} = await goog.requireDynamic('a.b.c');",
                "}")),
        expected(
            lines(
                "/** @const */ var module$exports$a$b$c = {};",
                "/** @const */ module$exports$a$b$c.Foo = class {}"),
            lines(
                "async function test() {",
                " await goog.importHandler_('sG5M4c');",
                " var {Foo} = module$exports$a$b$c;",
                "}")));
  }

  @Test
  public void testGoogRequireDynamic_then_destructuringPattern() {
    test(
        srcs(
            lines("goog.module('a.b.c');", "exports.Foo=class{}"),
            lines(
                "async function test() {", //
                "  goog.requireDynamic('a.b.c').then(({Foo}) => {console.log(Foo);});",
                "}")),
        expected(
            lines(
                "/** @const */ var module$exports$a$b$c = {};",
                "/** @const */ module$exports$a$b$c.Foo = class {}"),
            lines(
                "async function test() {", //
                "  goog.importHandler_('sG5M4c').then(() => { ",
                "     const {Foo} = module$exports$a$b$c;",
                "     console.log(Foo);",
                "  });",
                "}")));
  }

  @Test
  public void testGoogRequireDynamic_then_destructuringPattern_withExpressionBody() {
    test(
        srcs(
            lines("goog.module('a.b.c');", "exports.Foo=class{}"),
            lines(
                "async function test() {", //
                "  goog.requireDynamic('a.b.c').then(({Foo}) => Foo);",
                "}")),
        expected(
            lines(
                "/** @const */ var module$exports$a$b$c = {};",
                "/** @const */ module$exports$a$b$c.Foo = class {}"),
            lines(
                "async function test() {", //
                "  goog.importHandler_('sG5M4c').then(() => { ",
                "     const {Foo} = module$exports$a$b$c;",
                "     return Foo;",
                "  });",
                "}")));
    test(
        srcs(
            lines("goog.module('a.b.c');", "exports.Foo=class{}"),
            lines(
                "async function test() {", //
                "  goog.requireDynamic('a.b.c').then(({Foo}) => console.log(Foo));",
                "}")),
        expected(
            lines(
                "/** @const */ var module$exports$a$b$c = {};",
                "/** @const */ module$exports$a$b$c.Foo = class {}"),
            lines(
                "async function test() {", //
                "  goog.importHandler_('sG5M4c').then(() => { ",
                "     const {Foo} = module$exports$a$b$c;",
                "     return console.log(Foo);",
                "  });",
                "}")));
  }

  @Test
  public void testGoogRequireDynamic_then_name() {
    test(
        srcs(
            lines("goog.module('a.b.c');", "exports.Foo=class{}"),
            lines(
                "async function test() {", //
                "  goog.requireDynamic('a.b.c').then((foo) => {console.log(foo.Foo);});",
                "}")),
        expected(
            lines(
                "/** @const */ var module$exports$a$b$c = {};",
                "/** @const */ module$exports$a$b$c.Foo = class {}"),
            lines(
                "async function test() {", //
                "  goog.importHandler_('sG5M4c').then(() => { ",
                "     const foo = module$exports$a$b$c;",
                "     console.log(foo.Foo);",
                "  });",
                "}")));
  }

  @Test
  public void testRequireDynamic_illegal_await_lhs() {
    testError(
        srcs(
            lines("goog.module('a.b.c');", "exports.Foo=class{}"),
            lines("async function test() {", "await goog.requireDynamic('a.b.c');", "}")),
        ILLEGAL_STMT_OF_GOOG_REQUIRE_DYNAMIC_IN_AWAIT);
  }

  @Test
  public void testGoogModuleGet1() {
    test(
        srcs("goog.module('a');", lines("function f() {", "  var x = goog.module.get('a');", "}")),
        expected(
            "/** @const */ var module$exports$a = {};",
            lines("function f() {", "  var x = module$exports$a;", "}")));
  }

  @Test
  public void testGoogModuleGet2() {
    test(
        srcs(
            "goog.module('a.b.c');",
            lines("function f() {", "  var x = goog.module.get('a.b.c');", "}")),
        expected(
            "/** @const */ var module$exports$a$b$c = {};",
            lines("function f() {", "  var x = module$exports$a$b$c;", "}")));
  }

  @Test
  public void testGoogModuleGet3() {
    test(
        srcs(
            lines("goog.module('a.b.c');", "exports = class {};"),
            lines(
                "goog.module('x.y.z');",
                "",
                "function f() {",
                "  return new (goog.module.get('a.b.c'))();",
                "}")),
        expected(
            "/** @const */ var module$exports$a$b$c = class {};",
            lines(
                "/** @const */ var module$exports$x$y$z = {};",
                "function module$contents$x$y$z_f() {",
                "  return new module$exports$a$b$c();",
                "}")));
  }

  @Test
  public void testGoogModuleGet_missingInExpression() {
    allowExternsChanges();
    test(
        srcs(
            lines(
                "goog.module('x.y.z');",
                "",
                "function f() {",
                "  return goog.module.get('a.b.c');",
                "}",
                "f();")),
        expected(
            lines(
                "/** @const */",
                "var module$exports$x$y$z={};",
                "",
                "function module$contents$x$y$z_f() {",
                "  return null;",
                "}",
                "",
                "module$contents$x$y$z_f();")));
  }

  @Test
  public void testGoogModuleGet_missingAsRhs() {
    allowExternsChanges();
    test(
        srcs(
            lines(
                "goog.module('x.y.z');",
                "",
                "function f() {",
                "  const c = goog.module.get('a.b.c');",
                "  return c;",
                "}",
                "f();")),
        expected(
            lines(
                "/** @const */",
                "var module$exports$x$y$z={};",
                "",
                "function module$contents$x$y$z_f() {",
                "  return c;",
                "}",
                "",
                "module$contents$x$y$z_f();")));
  }

  @Test
  public void testGoogRequire_missing_createSyntheticExterns() {
    // typechecking reports errors due to us not including the Closure externs
    ignoreWarnings(DiagnosticGroups.CHECK_TYPES);

    testExternChanges(
        srcs(
            lines(
                "goog.module('mod');", //
                "const c = goog.require('a.b.c');",
                "c;")),
        expected("var c;"));

    //
    testExternChanges(
        srcs(
            lines(
                "goog.module('mod');", //
                "const {D: LocalD, E} = goog.require('a.b.c');",
                "LocalD;",
                "E")),
        expected("var LocalD;var E;"));
  }

  @Test
  public void testGoogModuleGet_missing_createSyntheticExterns() {
    // typechecking reports errors due to us not including the Closure externs
    ignoreWarnings(DiagnosticGroups.CHECK_TYPES);

    testExternChanges(
        srcs(
            lines(
                "(function() {",
                "  const c = goog.module.get('a.b.c');",
                "  const f = goog.module.get('d.e.f');",
                "})")),
        expected("var c;var f;"));

    // Declare the 'lhs' even when goog.module.get is nested within the right-hand side.
    testExternChanges(
        srcs(
            lines(
                "(function() {", //
                "  const result = process(goog.module.get('a.b.c').d) * 2;",
                "})")),
        expected("var result;"));

    // Don't declare any names multiple times.
    testExternChanges(
        srcs(
            lines(
                "(function() {",
                "  const c = goog.module.get('a.b.c');",
                "  c;",
                "  {",
                "    const c = goog.module.get('other.a.b.c');",
                "  }",
                "})")),
        expected("var c;"));
  }

  @Test
  public void testGoogModuleGet_missing_noSyntheticExternForExistingGlobals() {
    testNoWarning(
        srcs(
            lines(
                "var c;", //
                "(function() {",
                "  const c = goog.module.get('a.b.c');",
                "})")));

    testNoWarning(
        srcs(
            lines(
                "goog.provide('c');",
                "(function() {",
                "  const c = goog.module.get('a.b.c');",
                "})")));

    testNoWarning(
        srcs(
            lines(
                "goog.provide('c.d');",
                "(function() {",
                "  const c = goog.module.get('a.b.c');",
                "})")));

    testNoWarning(
        srcs(
            lines(
                "goog.module('c.d');", //
                "goog.module.declareLegacyNamespace();"),
            lines(
                "(function() {", //
                "  const c = goog.module.get('a.b.c');",
                "})")));
  }

  @Test
  public void testGoogModuleGet_missing_preserve() {
    preserveClosurePrimitives = true;
    // Need to disable tree comparison because compiler adds MODULE_BODY token when parsing
    // expected output but it is not present in actual tree.
    disableCompareAsTree();

    test(
        srcs(
            lines(
                "goog.module('x.y.z');",
                "",
                "function f() {",
                "  return goog.module.get('a.b.c');",
                "}",
                "f();")),
        expected(
            lines(
                "goog.module(\"x.y.z\");",
                "/** @const */ ",
                "var module$exports$x$y$z = {};",
                "function module$contents$x$y$z_f() {",
                "  return goog.module.get(\"a.b.c\");",
                "}",
                "module$contents$x$y$z_f();",
                "")));
  }

  @Test
  public void testAliasedGoogModuleGet1() {
    test(
        srcs(
            "goog.module('b'); exports = class {};",
            lines(
                "goog.module('a');",
                "var x = goog.forwardDeclare('b');",
                "function f() {",
                "  x = goog.module.get('b');",
                "  new x;",
                "}")),
        expected(
            "/** @const */ var module$exports$b = class {};",
            lines(
                "/** @const */ var module$exports$a = {};",
                "function module$contents$a_f() {",
                "  new module$exports$b;",
                "}")));
  }

  @Test
  public void testAliasedGoogModuleGet2() {
    test(
        srcs(
            "goog.module('x.y.z'); exports = class {};",
            lines(
                "goog.module('a');",
                "var x = goog.forwardDeclare('x.y.z');",
                "function f() {",
                "  x = goog.module.get('x.y.z');",
                "  new x;",
                "}")),
        expected(
            "/** @const */ var module$exports$x$y$z = class {};",
            lines(
                "/** @const */ var module$exports$a = {};",
                "function module$contents$a_f() {",
                "  new module$exports$x$y$z;",
                "}")));
  }

  @Test
  public void testAliasedGoogModuleGet3() {
    test(
        srcs(
            lines(
                "goog.module('a.b.c');", //
                "/** @constructor */ function C() {}",
                "exports = C"),
            lines(
                "/** @type {a.b.c} */ var c;",
                "function f() {",
                "  var C = goog.module.get('a.b.c');",
                "  c = new C;",
                "}")),
        expected(
            lines(
                "/** @constructor */ function module$contents$a$b$c_C() {}",
                "/** @const */ var module$exports$a$b$c = module$contents$a$b$c_C;"),
            lines(
                "/** @type {module$exports$a$b$c} */ var c;",
                "function f() {",
                "  var C = module$exports$a$b$c;",
                "  c = new C;",
                "}")));
  }

  @Test
  public void testAliasedGoogModuleGet4() {
    test(
        srcs(
            lines(
                "goog.module('x.y.z');", //
                "/** @constructor */ function Z() {}",
                "exports = Z"),
            lines(
                "goog.module('a');",
                "/** @type {z} */ var c;",
                "var z = goog.forwardDeclare('x.y.z');",
                "function f() {",
                "  z = goog.module.get('x.y.z');",
                "  c = new z;",
                "}")),
        expected(
            lines(
                "/** @constructor */ function module$contents$x$y$z_Z() {}",
                "/** @const */ var module$exports$x$y$z = module$contents$x$y$z_Z;"),
            lines(
                "/** @const */ var module$exports$a = {};",
                "/** @type {module$contents$a_z} */ var module$contents$a_c;",
                "function module$contents$a_f() {",
                "  module$contents$a_c = new module$exports$x$y$z;",
                "}")));
  }

  @Test
  public void testAliasedGoogModuleGet5() {
    test(
        srcs(
            "goog.provide('b'); b = class {};",
            lines(
                "goog.module('a');",
                "var x = goog.forwardDeclare('b');",
                "function f() {",
                "  x = goog.module.get('b');",
                "  new x;",
                "}")),
        expected(
            "goog.provide('b'); b = class {};",
            lines(
                "/** @const */ var module$exports$a = {};",
                "goog.forwardDeclare('b');",
                "function module$contents$a_f() {",
                "  new b;",
                "}")));
  }

  @Test
  public void testAliasedGoogModuleGet6() {
    test(
        srcs(
            "goog.provide('x.y.z'); x.y.z = class {};",
            lines(
                "goog.module('a');",
                "var z = goog.forwardDeclare('x.y.z');",
                "function f() {",
                "  z = goog.module.get('x.y.z');",
                "  new z;",
                "}")),
        expected(
            "goog.provide('x.y.z'); x.y.z = class {};",
            lines(
                "/** @const */ var module$exports$a = {};",
                "goog.forwardDeclare('x.y.z');",
                "function module$contents$a_f() {",
                "  new x.y.z;",
                "}")));
  }

  @Test
  public void testAliasedGoogModuleGet7() {
    test(
        srcs(
            "goog.module('a.b.c.D');",
            lines(
                "goog.require('a.b.c.D');",
                "goog.scope(function() {",
                "var D = goog.module.get('a.b.c.D');",
                "});")),
        expected(
            "/** @const */ var module$exports$a$b$c$D = {};",
            lines("goog.scope(function() {", "var D = module$exports$a$b$c$D;", "});")));
  }

  @Test
  public void testAliasedGoogModuleGet8() {
    test(
        srcs(
            "goog.module('a.b.c.D'); exports = class {};",
            lines(
                "goog.require('a.b.c.D');",
                "goog.scope(function() {",
                "var D = goog.module.get('a.b.c.D');",
                "var d = new D;",
                "});")),
        expected(
            "/** @const */ var module$exports$a$b$c$D = class {};",
            lines(
                "goog.scope(function() {",
                "var D = module$exports$a$b$c$D;",
                "var d = new D;",
                "});")));
  }

  @Test
  public void testAliasedGoogModuleGetInGoogProvide() {
    test(
        srcs(
            "goog.module('a.b.c.D');",
            lines(
                "goog.provide('x.y.z');",
                "goog.require('a.b.c.D');",
                "x.y.z = goog.module.get('a.b.c.D');",
                "")),
        expected(
            "/** @const */ var module$exports$a$b$c$D = {};",
            lines(
                "goog.provide('x.y.z');", //
                "x.y.z = module$exports$a$b$c$D;",
                "")));
  }

  @Test
  public void testInvalidGoogForwardDeclareParameter() {
    // Wrong parameter count.
    testError(
        lines("goog.module('a');", "var x = goog.forwardDeclare();"),
        INVALID_FORWARD_DECLARE_NAMESPACE);

    // Wrong parameter count.
    testError(
        lines("goog.module('a');", "var x = goog.forwardDeclare('a', 'b');"),
        INVALID_FORWARD_DECLARE_NAMESPACE);

    // Wrong parameter type.
    testError(
        lines("goog.module('a');", "var x = goog.forwardDeclare({});"),
        INVALID_FORWARD_DECLARE_NAMESPACE);
  }

  @Test
  public void testInvalidGoogModuleGet1() {
    testError(lines("function f() {", "  goog.module.get(a);", "}"), INVALID_GET_NAMESPACE);
  }

  @Test
  public void testInvalidGoogModuleGet2() {
    // This is checked earlier, in CheckClosureImports, not in the rewriting pass.
    testNoWarning(srcs("goog.module('a');", "goog.module.get('a');"));
  }

  @Test
  public void testExtractableExport1() {
    test(
        lines("goog.module('xid');", "var xid = function() {};", "exports = xid;"),
        "var module$exports$xid = function() {};");
  }

  @Test
  public void testExtractableExport2() {
    test(
        lines(
            "goog.module('xid');", //
            "function xid() {}",
            "exports = xid;"),
        lines(
            "function module$contents$xid_xid() {}",
            "/** @const */ var module$exports$xid = module$contents$xid_xid;"));
  }

  @Test
  public void testExtractableExport3() {
    test(
        lines("goog.module('Foo');", "class Foo {}", "exports = Foo;"),
        "class module$exports$Foo {}");
  }

  @Test
  public void testExtractableExport4() {
    test(
        lines("goog.module('Foo');", "const Foo = class {}", "exports = Foo;"),
        "const module$exports$Foo = class {};");
  }

  @Test
  public void testExport0() {
    test("goog.module('ns.a');", "/** @const */ var module$exports$ns$a = {};");
  }

  @Test
  public void testExport1() {
    test(
        lines("goog.module('ns.a');", "exports = {};"),
        "/** @const */ var module$exports$ns$a = {};");
  }

  @Test
  public void testExport2() {
    test(
        lines("goog.module('ns.a');", "exports.x = 1;"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @const */ module$exports$ns$a.x = 1"));
  }

  @Test
  public void testExport4() {
    test(
        lines("goog.module('ns.a');", "exports = { something: 1 };"),
        "/** @const */ var module$exports$ns$a = { /** @const */ something : 1 };");
  }

  @Test
  public void testExport5() {
    test(
        lines("goog.module('ns.a');", "/** @typedef {string} */ var x;", "exports.x = x;"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @typedef {string} */ module$exports$ns$a.x;"));
  }

  @Test
  public void testExport6() {
    test(
        lines(
            "goog.module('ns.a');",
            "/** @typedef {string} */ var x;",
            "exports = { something: x };"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @typedef {string} */ module$exports$ns$a.something;"));
  }

  @Test
  public void testExport6_1() {
    test(
        lines(
            "goog.module('ns.a');", //
            "/** @typedef {string} */ var x;",
            "exports.something = x;"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @typedef {string} */ module$exports$ns$a.something;"));
  }

  @Test
  public void testExport7() {
    test(
        lines("goog.module('ns.a');", "/** @constructor */", "exports = function() {};"),
        "/** @constructor @const */ var module$exports$ns$a = function() {};");
  }

  @Test
  public void testExport8() {
    test(
        lines("goog.module('ns.a');", "exports = goog.defineClass(null, {});"),
        "/** @const */ var module$exports$ns$a = goog.defineClass(null, {});");
  }

  @Test
  public void testExport9() {
    // Doesn't legacy-to-binary bridge export a typedef.
    testSame(
        lines(
            "goog.provide('goog.ui.ControlContent');",
            "/** @typedef {string} */ goog.ui.ControlContent;"));
  }

  @Test
  public void testExport10() {
    // Doesn't rewrite exports in legacy scripts.
    testSame(
        lines(
            "(function() {",
            "  /** @constructor */ function S(string) {}",
            "  exports.S = S;",
            "})();"));
  }

  @Test
  public void testExport11() {
    // Does rewrite export typedefs and defensively creates the exports root object first.
    test(
        lines("goog.module('a.B');", "/** @typedef {string} */ exports.C;"),
        lines(
            "/** @const */ var module$exports$a$B = {};",
            "/** @const @typedef {string} */ module$exports$a$B.C;"));
  }

  @Test
  public void testExport12() {
    test(
        lines("goog.module('ns.a');", "exports.foo = goog.defineClass(null, {});"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @const */ module$exports$ns$a.foo = goog.defineClass(null, {});"));
  }

  @Test
  public void testExport13() {
    // Creates the exports root object before export object reads.
    test(
        lines("goog.module('a.B');", "var field = exports;"),
        lines(
            "/** @const */ var module$exports$a$B = {};",
            "var module$contents$a$B_field = module$exports$a$B;"));
  }

  @Test
  public void testExport_initializedWithVar() {
    test(
        // TODO(lharker): should `var exports = ...` be banned?
        lines(
            "goog.module('ns.a');", //
            "/** @suppress {checkTypes} */",
            // this statement causes a "type mismatch" error
            "var exports = {};"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @suppress {checkTypes} */",
            "var module$contents$ns$a_exports = {};"));
  }

  @Test
  public void testExport_dontMangleLocalVariableNamedExports() {
    test(
        lines(
            "goog.module('ns.a');",
            "",
            "function f(exports, a) {",
            // test the various syntactic froms of doing goog.module exports
            "  exports.prop = 0;",
            "  exports = {a};",
            "  exports = function() {};",
            "  if (true) {",
            "    const exports = {};",
            "  }",
            "  return exports;",
            "}"),
        lines(
            "/** @const */",
            "var module$exports$ns$a = {};",
            "",
            "function module$contents$ns$a_f(exports, a) {",
            "  exports.prop = 0;",
            "  exports = {a};",
            "  exports = function() {};",
            "  if (true) {",
            "    const exports = {};",
            "  }",
            "  return exports;",
            "}"));
  }

  @Test
  public void testExportEnhancedObjectLiteral() {
    test(
        lines("goog.module('ns.a');", "class Something {}", "exports = { Something };"),
        lines(
            "/** @const */ var module$exports$ns$a = {};",
            "module$exports$ns$a.Something = class {};"));

    testError(
        lines("goog.module('ns.a');", "exports = { [something]: 3 };"),
        INVALID_EXPORT_COMPUTED_PROPERTY);
  }

  @Test
  public void testImport() {
    // A goog.module() that imports, jsdocs, and uses both another goog.module() and a legacy
    // script.
    test(
        srcs(
            lines("goog.module('p.A');", "/** @constructor */ function A() {}", "exports = A;"),
            lines("goog.provide('p.B');", "/** @constructor */ p.B = function() {}"),
            lines(
                "goog.module('p.C');",
                "var A = goog.require('p.A');",
                "var B = goog.require('p.B');",
                "function main() {",
                "  /** @type {A} */ var a = new A;",
                "  /** @type {B} */ var b = new B;",
                "}")),
        expected(
            lines(
                "/** @constructor */ function module$contents$p$A_A() {}",
                "/** @const */ var module$exports$p$A = module$contents$p$A_A;"),
            lines(
                "goog.provide('p.B');", //
                "/** @constructor */ p.B = function() {}"),
            lines(
                "/** @const */ var module$exports$p$C = {};",
                "goog.require('p.B');",
                "function module$contents$p$C_main() {",
                "  /** @type {module$exports$p$A} */ var a = new module$exports$p$A;",
                "  /** @type {p.B} */ var b = new p.B;",
                "}")));
  }

  @Test
  public void testSetTestOnly() {
    test(
        lines("goog.module('ns.a');", "goog.setTestOnly();"),
        lines("/** @const */ var module$exports$ns$a = {};", "goog.setTestOnly();"));
  }

  @Test
  public void testRewriteJsDocImportedType() {
    test(
        srcs(
            lines("goog.module('p.A');", "/** @constructor */", "function A() {}", "exports = A;"),
            lines("goog.module('p.B');", "/** @constructor */", "function B() {}", "exports = B;"),
            lines(
                "goog.module('p.C');",
                "var A = goog.require('p.A');",
                "var B = goog.requireType('p.B');",
                "function main() {",
                "  /** @type {A} */",
                "  var a = new A;",
                "  /** @type {B} */",
                "  var b;",
                "}")),
        expected(
            lines(
                "/** @constructor */ function module$contents$p$A_A() {}",
                "/** @const */ var module$exports$p$A = module$contents$p$A_A;"),
            lines(
                "/** @constructor */ function module$contents$p$B_B() {}",
                "/** @const */ var module$exports$p$B = module$contents$p$B_B;"),
            lines(
                "/** @const */ var module$exports$p$C = {};",
                "function module$contents$p$C_main() {",
                "  /** @type {module$exports$p$A} */",
                "  var a = new module$exports$p$A;",
                "  /** @type {module$exports$p$B} */",
                "  var b;",
                "}")));
  }

  @Test
  public void testRewriteJsDocOwnType() {
    test(
        srcs(
            lines(
                "goog.module('p.b');",
                "/** @constructor */",
                "function B() {}",
                "function main() {",
                "  /** @type {B} */",
                "  var b = new B;",
                "}")),
        expected(
            lines(
                "/** @const */ var module$exports$p$b = {};",
                "/** @constructor */",
                "function module$contents$p$b_B() {}",
                "function module$contents$p$b_main() {",
                "  /** @type {module$contents$p$b_B} */",
                "  var b = new module$contents$p$b_B;",
                "}")));
  }

  @Test
  public void testRewriteJsDocFullyQualified() {
    test(
        srcs(
            lines("goog.module('p.A');", "/** @constructor */", "function A() {}", "exports = A;"),
            lines("goog.module('p.B');", "/** @constructor */", "function B() {}", "exports = B;"),
            lines(
                "goog.module('p.C');",
                "var A = goog.require('p.A');",
                "var B = goog.requireType('p.B');",
                "function main() {",
                "  /** @type {p.A} */",
                "  var a = new A;",
                "  /** @type {p.B} */",
                "  var b;",
                "}")),
        expected(
            lines(
                "/** @constructor */",
                "function module$contents$p$A_A() {}",
                "/** @const */ var module$exports$p$A = module$contents$p$A_A;"),
            lines(
                "/** @constructor */",
                "function module$contents$p$B_B() {}",
                "/** @const */ var module$exports$p$B = module$contents$p$B_B;"),
            lines(
                "/** @const */ var module$exports$p$C = {};",
                "function module$contents$p$C_main() {",
                "  /** @type {module$exports$p$A} */",
                "  var a = new module$exports$p$A;",
                "  /** @type {module$exports$p$B} */",
                "  var b;",
                "}")));
  }

  @Test
  public void testRewriteJsDocCircularReferenceWithRequireType() {
    test(
        srcs(
            lines(
                "goog.module('p.A');",
                "goog.requireType('p.B');",
                "/** @constructor */",
                "function A() {}",
                "A.prototype.setB = function(/** p.B */ x) {}",
                "exports = A;"),
            lines(
                "goog.module('p.B');",
                "var A = goog.require('p.A');",
                "/** @constructor @extends {A} */",
                "function B() {}",
                "B.prototype = new A;",
                "exports = B;")),
        expected(
            lines(
                "/** @constructor */",
                "function module$contents$p$A_A() {}",
                "module$contents$p$A_A.prototype.setB = function(/** module$exports$p$B */ x) {}",
                "/** @const */ var module$exports$p$A = module$contents$p$A_A;"),
            lines(
                "/** @constructor @extends {module$exports$p$A} */",
                "function module$contents$p$B_B() {}",
                "module$contents$p$B_B.prototype = new module$exports$p$A;",
                "/** @const */ var module$exports$p$B = module$contents$p$B_B;")));
  }

  @Test
  public void testRewriteJsDocCircularReferenceWithoutRequireType() {
    test(
        srcs(
            lines(
                "goog.module('p.A');",
                "/** @constructor */",
                "function A() {}",
                "A.prototype.setB = function(/** p.B */ x) {}",
                "exports = A;"),
            lines(
                "goog.module('p.B');",
                "var A = goog.require('p.A');",
                "/** @constructor @extends {A} */",
                "function B() {}",
                "B.prototype = new A;",
                "exports = B;")),
        expected(
            lines(
                "/** @constructor */",
                "function module$contents$p$A_A() {}",
                "module$contents$p$A_A.prototype.setB = function(/** module$exports$p$B */ x) {}",
                "/** @const */ var module$exports$p$A = module$contents$p$A_A;"),
            lines(
                "/** @constructor @extends {module$exports$p$A} */",
                "function module$contents$p$B_B() {}",
                "module$contents$p$B_B.prototype = new module$exports$p$A;",
                "/** @const */ var module$exports$p$B = module$contents$p$B_B;")));
  }

  @Test
  public void testRewriteJsDocOwnTypeExported() {
    test(
        lines(
            "goog.module('p.A');",
            "",
            "/** @constructor */",
            "function A() {}",
            "",
            "/** @type {!A} */",
            "var x = new A;",
            "",
            "exports = A;"),
        lines(
            "/** @constructor */",
            "function module$contents$p$A_A() {}",
            "/** @type {!module$contents$p$A_A} */",
            "var module$contents$p$A_x = new module$contents$p$A_A;",
            "/** @const */ var module$exports$p$A = module$contents$p$A_A;"));
  }

  @Test
  public void testDuplicateModuleDoesntCrash() {
    ignoreWarnings(DUPLICATE_MODULE, ILLEGAL_MODULE_RENAMING_CONFLICT);
    test(
        srcs("goog.module('ns.a');", "goog.module('ns.a');"),
        expected(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @const */ var module$exports$ns$a = {};"));
  }

  @Test
  public void testDuplicateRequireDoesntCrash() {
    ignoreWarnings(DiagnosticGroups.DUPLICATE_VARS);
    test(
        srcs(
            lines("goog.module('ns.b');", "exports = 1;"),
            lines(
                "goog.module('ns.a');",
                "const b = goog.require('ns.b');",
                "const b = goog.require('ns.b');",
                "console.log(b);")),
        expected(
            lines("/** @const */", "var module$exports$ns$b = 1;"),
            lines(
                "/** @const */",
                "var module$exports$ns$a = {};",
                "console.log(module$exports$ns$b);")));
  }

  @Test
  public void testDuplicateBundledModuleDoesntCrash() {
    ignoreWarnings(DUPLICATE_MODULE, ILLEGAL_MODULE_RENAMING_CONFLICT);
    String bundledModule =
        "goog.loadModule(function(exports) { goog.module('ns.a'); return exports; });";
    test(
        srcs(bundledModule, bundledModule),
        expected(
            "/** @const */ var module$exports$ns$a = {};",
            "/** @const */ var module$exports$ns$a = {};"));
  }

  @Test
  public void testDuplicateNamespaceDoesntCrash() {
    // The compiler emits a warning elsewhere for this code
    testError(
        srcs("goog.module('ns.a');", "goog.provide('ns.a');"), DUPLICATE_NAMESPACE_AND_MODULE);
  }

  @Test
  public void testMisplacedGoogModuleDoesntCrash() {
    // The compiler emits a warning elsewhere for this code
    test(
        srcs("alert(goog.module('ns.a'));"), //
        expected("alert(void 0);"));
  }

  @Test
  public void testImportInliningDoesntShadow() {
    testNoWarning(
        lines(
            "goog.provide('a.b.c');",
            "/** @const */ var a = a || {};",
            "a.b.c = class {};",
            "goog.loadModule(function(exports) { 'use strict';",
            "  goog.module('a.b.d');",
            "  goog.module.declareLegacyNamespace();",
            "  var c = goog.require('a.b.c');",
            "  exports.c = new c;",
            "  return exports;",
            "});"));

    testNoWarning(
        lines(
            "goog.provide('a.b.c');",
            "/** @const */ var a = a || {};",
            "a.b.c = class {};",
            "goog.loadModule(function(exports) { 'use strict';",
            "  goog.module('a.b.d');",
            "  goog.module.declareLegacyNamespace();",
            "  var c = goog.requireType('a.b.c');",
            "  exports.c = new c;",
            "  return exports;",
            "});"));
  }

  @Test
  public void testImportInliningShadowsVar() {

    testError(
        srcs(
            lines(
                "goog.provide('a.b.c');", //
                "a.b.c = 5;"),
            lines(
                "goog.module('a.b.d');",
                "var c = goog.require('a.b.c');",
                "function foo() {",
                "  var a = 10;",
                "  var b = c;",
                "}")),
        IMPORT_INLINING_SHADOWS_VAR);

    testError(
        srcs(
            lines(
                "goog.provide('a.b.c');", //
                "a.b.c = 5;"),
            lines(
                "goog.module('a.b.d');",
                "var c = goog.requireType('a.b.c');",
                "function foo() {",
                "  var a = 10;",
                "  var b = c;",
                "}")),
        IMPORT_INLINING_SHADOWS_VAR);
  }

  @Test
  public void testImportInliningShadowsDestructuredImport() {
    testError(
        srcs(
            lines("goog.provide('a.b.c');", "a.b.c.d = 5;"),
            lines(
                "goog.module('a.b.d');",
                "const {d} = goog.require('a.b.c');",
                "function foo() {",
                "  var a = 10;",
                "  var b = d;",
                "}")),
        IMPORT_INLINING_SHADOWS_VAR);

    testError(
        srcs(
            lines("goog.provide('a.b.c');", "a.b.c.d = 5;"),
            lines(
                "goog.module('a.b.d');",
                "const {d} = goog.requireType('a.b.c');",
                "function foo() {",
                "  var a = 10;",
                "  var b = d;",
                "}")),
        IMPORT_INLINING_SHADOWS_VAR);
  }

  @Test
  public void testExportsShadowingAllowed() {
    testNoWarning(
        lines(
            "goog.loadModule(function(exports) {",
            "   goog.module('a.b.c');",
            "",
            "   class Foo {}",
            "   /** @const {*} */ Foo.prototype.x;",
            "   exports.Foo = Foo;",
            "",
            "   return exports;",
            "});"));
  }

  @Test
  public void testExportRewritingShadows() {
    test(
        lines(
            "goog.module('a.b.c');",
            "function test() {}",
            "function f(test) { return test; }",
            "exports = test;"),
        lines(
            "function module$contents$a$b$c_test() {}",
            "function module$contents$a$b$c_f(test) { return test; }",
            "/** @const */ var module$exports$a$b$c = module$contents$a$b$c_test;"));

    test(
        lines(
            "goog.module('a.b.c');",
            "function test() {}",
            "function f(test) { return test; }",
            "exports.test = test;"),
        lines(
            "/** @const */ var module$exports$a$b$c = {};",
            "function module$contents$a$b$c_test() {}",
            "function module$contents$a$b$c_f(test) { return test; }",
            "/** @const */ module$exports$a$b$c.test = module$contents$a$b$c_test;"));
  }

  @Test
  public void testEarlyRequireModule() {
    test(srcs(lines("goog.module('ns.a');", "goog.require('ns.b')"), "goog.module('ns.b');"));
  }

  @Test
  public void testEarlyRequireLegacyScript() {
    test(srcs(lines("goog.module('ns.a');", "goog.require('ns.b')"), "goog.provide('ns.b');"));
  }

  @Test
  public void testEarlyRequireTypeModule() {
    test(srcs(lines("goog.module('ns.a');", "goog.requireType('ns.b')"), "goog.module('ns.b');"));
  }

  @Test
  public void testEarlyRequireTypeLegacyScript() {
    test(srcs(lines("goog.module('ns.a');", "goog.requireType('ns.b')"), "goog.provide('ns.b');"));
  }

  @Test
  public void testValidEarlyGoogModuleGet() {
    // Legacy Script to Module goog.module.get.
    test(
        srcs(
            lines(
                "goog.provide('ns.a');",
                "function foo() {",
                "  var b = goog.module.get('ns.b');",
                "}"),
            "goog.module('ns.b');"),
        expected(
            "goog.provide('ns.a'); function foo() { var b = module$exports$ns$b; }",
            "/** @const */ var module$exports$ns$b = {};"));
  }

  @Test
  public void testInnerScriptOuterModule() {
    // Rewrites fully qualified JsDoc references to types but without writing a prefix as
    // module$exports when there's a longer prefix that references a script.

    // Test when the prefix is chunk unit longer than the module name
    test(
        srcs(
            lines(
                "goog.module('A');", //
                "/** @constructor */",
                "function A() {}",
                "exports = A;"),
            lines(
                "goog.provide('A.B');",
                "/** @constructor */",
                "A.B = function () {}",
                "function main() {",
                "  /** @type {A.B} */",
                "  var l = new A.B();",
                "}")),
        expected(
            lines(
                "/** @constructor */ function module$contents$A_A() {}",
                "/** @const */ var module$exports$A = module$contents$A_A;"),
            lines(
                "goog.provide('A.B');",
                "/** @constructor */",
                "A.B = function() {};",
                "function main() {",
                // Note A.B was NOT written to module$exports$A.B
                "  /** @type {A.B} */",
                "  var l = new A.B();",
                "}")));

    // Test when the prefix is much longer than the module name
    test(
        srcs(
            lines(
                "goog.module('A');", //
                "/** @constructor */",
                "function A() {}",
                "exports = A;"),
            lines(
                "goog.provide('A.b.c.D');",
                "/** @constructor */",
                "A.b.c.L = function () {}",
                "function main() {",
                "  /** @type {A.b.c.L} */",
                "  var l = new A.b.c.L();",
                "}")),
        expected(
            lines(
                "/** @constructor */ function module$contents$A_A() {}",
                "/** @const */ var module$exports$A = module$contents$A_A;"),
            lines(
                "goog.provide('A.b.c.D');",
                "/** @constructor */",
                "A.b.c.L = function() {};", // Note L not D
                "function main() {",
                // Note A.b.c.L was NOT written to module$exports$A.b.c.L.
                "  /** @type {A.b.c.L} */",
                "  var l = new A.b.c.L();",
                "}")));
  }

  @Test
  public void testModuleLevelVars() {
    test(
        lines("goog.module('b.c.c');", "/** @const */", "var F = 0;"),
        lines(
            "/** @const */ var module$exports$b$c$c = {};",
            "/** @const */ var module$contents$b$c$c_F = 0;"));
  }

  @Test
  public void testPublicExport() {
    test(
        lines(
            "goog.module('a.b.c');",
            "goog.module.declareLegacyNamespace();",
            "/** @public */ exports = 5;"),
        lines("goog.provide('a.b.c');", "/** @const @public */ a.b.c = 5;"));
  }

  @Test
  public void testReferenceToNonLegacyGoogModuleName() {
    test(
        srcs("goog.module('a.b.c');", "use(a.b.c);"),
        expected("/** @const */ var module$exports$a$b$c={}", "use(a.b.c);"),
        warning(TypeCheck.POSSIBLE_INEXISTENT_PROPERTY),
        warning(TypeCheck.POSSIBLE_INEXISTENT_PROPERTY));
  }

  @Test
  public void testGoogModuleValidReferences1() {
    test(
        srcs(
            "goog.module('a.b.c');",
            lines(
                "goog.module('x.y.z');", //
                "var c = goog.require('a.b.c');",
                "use(c);")),
        expected(
            "/** @const */ var module$exports$a$b$c={};",
            lines(
                "/** @const */ var module$exports$x$y$z={};", //
                "use(module$exports$a$b$c);")));
  }

  @Test
  public void testGoogModuleValidReferences2() {
    test(
        srcs(
            "goog.module('a.b.c');",
            lines(
                "goog.require('a.b.c');",
                "goog.scope(function() {",
                "  var c = goog.module.get('a.b.c');",
                "  use(c);",
                "});")),
        expected(
            "/** @const */ var module$exports$a$b$c={};",
            "goog.scope(function() { var c = module$exports$a$b$c; use(c); });"));
  }

  @Test
  public void testLegacyGoogModuleValidReferences() {
    test(
        srcs(
            "goog.module('a.b.c'); goog.module.declareLegacyNamespace();",
            "goog.require('a.b.c'); use(a.b.c);"),
        expected("goog.provide('a.b.c');", "goog.require('a.b.c'); use(a.b.c);"));

    test(
        srcs(
            "goog.module('a.b.c'); goog.module.declareLegacyNamespace();",
            "goog.module('x.y.z'); var c = goog.require('a.b.c'); use(c);"),
        expected(
            "goog.provide('a.b.c');",
            "/** @const */ var module$exports$x$y$z={}; goog.require('a.b.c'); use(a.b.c);"));

    test(
        srcs(
            lines(
                "goog.module('a.b.Foo');",
                "goog.module.declareLegacyNamespace();",
                "",
                "/** @constructor */ exports = function() {};"),
            "/** @param {a.b.Foo} x */ function f(x) {}"),
        expected(
            "goog.provide('a.b.Foo'); /** @constructor @const */ a.b.Foo = function() {};",
            "/** @param {a.b.Foo} x */ function f(x) {}"));

    test(
        srcs(
            lines(
                "goog.module('a.b.c');",
                "goog.module.declareLegacyNamespace();",
                "",
                "exports = function() {};"),
            "function f() { return goog.module.get('a.b.c'); }"),
        expected(
            "goog.provide('a.b.c'); /** @const */ a.b.c = function() {};",
            "function f() { return a.b.c; }"));

    test(
        srcs(
            lines(
                "goog.module('a.b.Foo');",
                "goog.module.declareLegacyNamespace();",
                "",
                "/** @constructor */ function Foo() {}",
                "",
                "exports = Foo;"),
            "/** @param {a.b.Foo} x */ function f(x) {}"),
        expected(
            lines(
                "goog.provide('a.b.Foo');",
                "/** @constructor */ function module$contents$a$b$Foo_Foo() {}",
                "/** @const */ a.b.Foo = module$contents$a$b$Foo_Foo;"),
            "/** @param {a.b.Foo} x */ function f(x) {}"));

    test(
        srcs(
            lines(
                "goog.module('a.b');",
                "goog.module.declareLegacyNamespace();",
                "",
                "/** @constructor */ function Foo() {};",
                "",
                "exports.Foo = Foo;"),
            "/** @param {a.b.Foo} x */ function f(x) {}"),
        expected(
            lines(
                "goog.provide('a.b');",
                "/** @constructor */ function module$contents$a$b_Foo() {};",
                "/** @const */ a.b.Foo = module$contents$a$b_Foo;"),
            "/** @param {a.b.Foo} x */ function f(x) {}"));
  }

  @Test
  public void testUselessUseStrict() {
    testWarning(
        "'use strict'; goog.module('b.c.c');", ClosureRewriteModule.USELESS_USE_STRICT_DIRECTIVE);
  }

  @Test
  public void testRewriteGoogModuleAliases1() {
    test(
        srcs(
            lines(
                "goog.module('base');",
                "",
                "/** @constructor */ var Base = function() {}",
                "exports = Base;"),
            lines(
                "goog.module('leaf');",
                "",
                "var Base = goog.require('base');",
                "exports = /** @constructor @extends {Base} */ function Foo() {}")),
        expected(
            "/** @constructor */ var module$exports$base = function() {};",
            lines(
                "/** @const */ var module$exports$leaf = ",
                "/** @constructor @extends {module$exports$base} */ function Foo() {}")));
  }

  @Test
  public void testRewriteGoogModuleAliases2() {
    test(
        srcs(
            lines(
                "goog.module('ns.base');",
                "",
                "/** @constructor */ var Base = function() {}",
                "exports = Base;"),
            lines(
                "goog.module('leaf');",
                "",
                "var Base = goog.require('ns.base');",
                "exports = /** @constructor @extends {Base} */ function Foo() {}")),
        expected(
            "/** @constructor */ var module$exports$ns$base = function() {};",
            lines(
                "/** @const */ var module$exports$leaf = ",
                "/** @constructor @extends {module$exports$ns$base} */ function Foo() {}")));
  }

  @Test
  public void testRewriteGoogModuleAliases3() {
    test(
        srcs(
            lines(
                "goog.module('ns.base');",
                "",
                "/** @constructor */ var Base = function() {};",
                "/** @constructor */ Base.Foo = function() {};",
                "exports = Base;"),
            lines(
                "goog.module('leaf');",
                "",
                "var Base = goog.require('ns.base');",
                "exports = /** @constructor @extends {Base.Foo} */ function Foo() {}")),
        expected(
            lines(
                "/** @constructor */ var module$exports$ns$base = function() {};",
                "/** @constructor */ module$exports$ns$base.Foo = function() {};"),
            lines(
                "/** @const */ var module$exports$leaf = ",
                "/** @constructor @extends {module$exports$ns$base.Foo} */ function Foo() {}")));
  }

  @Test
  public void testRewriteGoogModuleAliases4() {
    test(
        srcs(
            lines(
                "goog.module('ns.base');",
                "",
                "/** @constructor */ var Base = function() {}",
                "exports = Base;"),
            lines(
                "goog.module('leaf');",
                "",
                "var Base = goog.require('ns.base');",
                "exports = new Base;")),
        expected(
            "/** @constructor */ var module$exports$ns$base = function() {};",
            "/** @const */ var module$exports$leaf = new module$exports$ns$base;"));
  }

  @Test
  public void testRewriteGoogModuleAliases5() {
    test(
        srcs(
            lines(
                "goog.module('ns.base');",
                "",
                "/** @constructor */ var Base = function() {}",
                "exports = Base;"),
            lines(
                "goog.module('mid');",
                "",
                "var Base = goog.require('ns.base');",
                "exports = Base;"),
            lines("goog.module('leaf')", "var Base = goog.require('mid');", "new Base;")),
        expected(
            "/** @constructor */ var module$exports$ns$base = function() {};",
            "/** @const */ var module$exports$mid = module$exports$ns$base;",
            "/** @const */ var module$exports$leaf = {}; new module$exports$mid;"));
  }

  @Test
  public void testRewriteGoogModuleAliases6() {
    test(
        srcs(
            lines("goog.module('base');", "", "/** @constructor */ exports.Foo = function() {};"),
            lines(
                "goog.module('FooWrapper');",
                "",
                "const {Foo} = goog.require('base');",
                "exports = Foo;")),
        expected(
            lines(
                "/** @const */ var module$exports$base = {};",
                "/** @constructor @const */ module$exports$base.Foo = function() {};"),
            "/** @const */ var module$exports$FooWrapper = module$exports$base.Foo;"));
  }

  @Test
  public void testRewriteGoogModuleAliases7() {
    test(
        srcs(
            lines("goog.module('base');", "", "/** @constructor */ exports.Foo = function() {};"),
            lines(
                "goog.module('FooWrapper');",
                "",
                "const {Foo: FooFromBaseModule} = goog.require('base');",
                "exports = FooFromBaseModule;")),
        expected(
            lines(
                "/** @const */ var module$exports$base = {};",
                "/** @constructor @const */ module$exports$base.Foo = function() {};"),
            "/** @const */ var module$exports$FooWrapper = module$exports$base.Foo;"));
  }

  @Test
  public void testRewriteGoogModuleTypes_localShadowsModuleScopeVar() {
    test(
        lines(
            "goog.module('client');", //
            "class Foo {}",
            "{",
            "  class Foo {}",
            "  let /** !Foo */ x;",
            "}"),
        lines(
            "/** @const */ var module$exports$client = {};", //
            "class module$contents$client_Foo {}",
            "{",
            "  class Foo {}",
            "  let /** !Foo */ x;",
            "}"));
  }

  @Test
  public void testRewriteGoogModuleTypes_localShadowsProvide() {
    test(
        srcs(
            "goog.provide('Foo.Child');",
            "goog.module('Bar'); exports.Child = class {};",
            lines(
                "goog.module('client');", //
                "const Foo = goog.require('Bar');",
                "let /** !Foo.Child */ myStr;")), // Foo.Child refers to the local, not the provide.
        expected(
            "goog.provide('Foo.Child');",
            lines(
                "/** @const */ var module$exports$Bar = {};", //
                "/** @const */ module$exports$Bar.Child = class {};"),
            lines(
                "/** @const */ var module$exports$client = {};", //
                "let /** !module$exports$Bar.Child */ module$contents$client_myStr;")));
  }

  @Test
  public void testRewriteGoogModuleTypes_localShadowsModule() {
    test(
        srcs(
            "goog.module('Foo.Child');",
            "goog.module('Bar'); exports.Child = class {};",
            lines(
                "goog.module('client');", //
                "const Foo = goog.require('Bar');",
                "let /** !Foo.Child */ myStr;")), // Foo.Child refers to the local, not the module.
        expected(
            "/** @const */ var module$exports$Foo$Child = {}",
            lines(
                "/** @const */ var module$exports$Bar = {};", //
                "/** @const */ module$exports$Bar.Child = class {};"),
            lines(
                "/** @const */ var module$exports$client = {};", //
                "let /** !module$exports$Bar.Child */ module$contents$client_myStr;")));
  }

  @Test
  public void testRewriteGoogModuleAliasesWithPreservedPrimitives() {
    preserveClosurePrimitives = true;
    // Need to disable tree comparison because compiler adds MODULE_BODY token when parsing
    // expected output but it is not present in actual tree.
    disableCompareAsTree();
    test(
        srcs(
            lines(
                "goog.module('foo');", //
                "/** @constructor */ exports = function() {};"),
            lines(
                "goog.module('bar');", //
                "exports.doBar = function() {};"),
            lines(
                "goog.module('baz');", //
                "exports.doBaz = function() {};"),
            lines(
                "goog.module('leaf1');",
                "var Foo = goog.require('foo');",
                "var {doBar} = goog.require('bar');",
                "var {doBaz: doooBaz} = goog.require('baz');"),
            lines(
                "goog.module('leaf2');",
                "var Foo = goog.requireType('foo');",
                "var {doBar} = goog.requireType('bar');",
                "var {doBaz: doooBaz} = goog.requireType('baz');")),
        expected(
            lines(
                "goog.module(\"foo\");", //
                "/** @const @constructor */ ",
                "var module$exports$foo = function() {",
                "};",
                ""),
            lines(
                "goog.module(\"bar\");",
                "/** @const */ ",
                "var module$exports$bar = {};",
                "/** @const */ ",
                "module$exports$bar.doBar = function() {",
                "};",
                ""),
            lines(
                "goog.module(\"baz\");",
                "/** @const */ ",
                "var module$exports$baz = {};",
                "/** @const */ ",
                "module$exports$baz.doBaz = function() {",
                "};",
                ""),
            lines(
                "goog.module(\"leaf1\");",
                "/** @const */ ",
                "var module$exports$leaf1 = {};",
                "var module$contents$leaf1_Foo = goog.require(\"foo\");",
                "var {doBar:module$contents$leaf1_doBar} = goog.require(\"bar\");",
                "var {doBaz:module$contents$leaf1_doooBaz} = goog.require(\"baz\");",
                ""),
            lines(
                "goog.module(\"leaf2\");",
                "/** @const */ ",
                "var module$exports$leaf2 = {};",
                "var module$contents$leaf2_Foo = goog.requireType(\"foo\");",
                "var {doBar:module$contents$leaf2_doBar} = goog.requireType(\"bar\");",
                "var {doBaz:module$contents$leaf2_doooBaz} = goog.requireType(\"baz\");",
                "")));
  }

  @Test
  public void testGoogModuleExportsProvidedName() {
    test(
        srcs(
            lines("goog.provide('Foo');", "", "/** @constructor */ var Foo = function() {};"),
            lines("goog.module('FooWrapper');", "", "goog.require('Foo');", "", "exports = Foo;")),
        expected(
            lines("goog.provide('Foo');", "", "/** @constructor */ var Foo = function() {};"),
            "goog.require('Foo'); /** @const */ var module$exports$FooWrapper = Foo;"));
  }

  @Test
  public void testRewriteGoogModuleAliasesWithPrototypeGets1() {
    test(
        srcs(
            lines(
                "goog.module('mod_B');",
                "",
                "/** @interface */ function B(){}",
                "B.prototype.f = function(){};",
                "",
                "exports = B;"),
            lines(
                "goog.module('mod_A');",
                "",
                "var B = goog.require('mod_B');",
                "",
                "/** @type {B} */",
                "var b;")),
        expected(
            lines(
                "/**@interface */ function module$contents$mod_B_B() {}",
                "module$contents$mod_B_B.prototype.f = function() {};",
                "/** @const */ var module$exports$mod_B = module$contents$mod_B_B;"),
            lines(
                "/** @const */ var module$exports$mod_A = {};",
                "/**@type {module$exports$mod_B} */ var module$contents$mod_A_b;")));
  }

  @Test
  public void testRewriteGoogModuleAliasesWithPrototypeGets2() {
    test(
        srcs(
            lines(
                "goog.module('mod_B');", //
                "",
                "/** @interface */ function B(){}",
                "",
                "exports = B;"),
            lines(
                "goog.module('mod_A');",
                "",
                "var B = goog.require('mod_B');",
                "B.prototype;",
                "",
                "/** @type {B} */",
                "var b;")),
        expected(
            lines(
                "/**@interface */ function module$contents$mod_B_B() {}",
                "/** @const */ var module$exports$mod_B = module$contents$mod_B_B;"),
            lines(
                "/** @const */ var module$exports$mod_A = {}",
                "module$exports$mod_B.prototype;",
                "/**@type {module$exports$mod_B} */ var module$contents$mod_A_b;")));
  }

  @Test
  public void testLegacyModuleIsUninlined() {
    test(
        lines(
            "goog.module('mod.ns');",
            "goog.module.declareLegacyNamespace();",
            "",
            "class Foo {}",
            "",
            "exports.Foo = Foo;"),
        lines(
            "goog.provide('mod.ns');",
            "class module$contents$mod$ns_Foo {}",
            "/** @const */ mod.ns.Foo = module$contents$mod$ns_Foo;"));
  }

  @Test
  public void testLegacyModuleExportStillExported() {
    test(
        lines(
            "goog.module('modA');",
            "goog.module.declareLegacyNamespace();",
            "",
            "class Foo {}",
            "exports = { /** @export */ Foo};"),
        lines(
            "goog.provide('modA');",
            "class module$contents$modA_Foo {}",
            "/** @const @export */ modA.Foo = module$contents$modA_Foo;"));
  }

  @Test
  public void testMultiplyExportedSymbolDoesntCrash() {
    test(
        lines(
            "goog.module('mod');",
            "",
            "class Foo {}",
            "",
            "exports.name1 = Foo;",
            "exports.name2 = Foo;"),
        lines(
            "/** @const */ var module$exports$mod = {};",
            "module$exports$mod.name1 = class {};",
            "/** @const */ module$exports$mod.name2 = module$exports$mod.name1;"));
  }

  @Test
  public void testIjsFileInExterns() {
    ignoreWarnings(TypeCheck.POSSIBLE_INEXISTENT_PROPERTY);
    allowExternsChanges();
    testNoWarning(
        externs(
            lines(
                "/** @typeSummary */",
                "goog.module('mod_B');",
                "",
                "/** @interface */ function B(){}",
                "",
                "exports = B;")),
        srcs(
            lines(
                "goog.module('mod_A');",
                "",
                "var B = goog.require('mod_B');",
                "",
                "/** @constructor @implements {B} */",
                "function A() {}")));

    testNoWarning(
        externs(
            lines(
                "/** @typeSummary */",
                "goog.loadModule(function(exports) { 'use strict';",
                "  goog.module('mod_B');",
                "",
                "  /** @interface */ function B(){}",
                "",
                "  exports = B;",
                "  return exports;",
                "});")),
        srcs(
            lines(
                "goog.module('mod_A');",
                "",
                "var B = goog.require('mod_B');",
                "",
                "/** @constructor @implements {B} */",
                "function A() {}")));

    testNoWarning(
        externs(
            lines(
                "/** @typeSummary */",
                "goog.loadModule(function(exports) { 'use strict';",
                "  goog.module('mod_B');",
                "",
                "  /** @interface */ function B(){}",
                "",
                "  exports.B = B;",
                "  return exports;",
                "});")),
        srcs(
            lines(
                "goog.module('mod_A');",
                "",
                "var {B} = goog.require('mod_B');",
                "",
                "/** @constructor @implements {B} */",
                "function A() {}")));
  }

  @Test
  public void addFreeCallToNamedExports() {
    disableCompareAsTree(); // necessary to compare 'free call' marking
    test(
        srcs(
            lines(
                "goog.module('mod');", //
                "exports.fn = function() {};"),
            lines(
                "goog.module('client');", //
                "const mod = goog.require('mod');",
                "mod.fn();")),
        expected(
            lines(
                "/** @const */ ",
                "var module$exports$mod = {};",
                "/** @const */ ",
                "module$exports$mod.fn = function() {",
                "};",
                "/** @const */ ",
                "var module$exports$client = {};",
                "(0,module$exports$mod.fn)();",
                "")));
  }

  @Test
  public void addFreeCallToNamedExportsLiteral() {
    disableCompareAsTree(); // necessary to compare 'free call' marking
    test(
        srcs(
            lines(
                "goog.module('mod');", //
                "const fn = function() {};",
                "exports = {fn}"),
            lines(
                "goog.module('client');", //
                "const mod = goog.require('mod');",
                "mod.fn();")),
        expected(
            lines(
                "/** @const */ ", //
                "var module$exports$mod = {};",
                "/** @const */ ",
                "module$exports$mod.fn = function() {",
                "};",
                "/** @const */ ",
                "var module$exports$client = {};",
                "(0,module$exports$mod.fn)();",
                "")));
  }

  @Test
  public void dontAddFreeCallToPropertiesOnDefaultExports() {
    test(
        srcs(
            lines(
                "goog.module('mod.Bar');", //
                "/** @const */",
                "exports = class Bar {",
                "  static fn() { use(this); }",
                "}"),
            lines(
                "goog.module('client');", //
                "const Bar = goog.require('mod.Bar');",
                "Bar.fn();")),
        expected(
            lines(
                "/** @const */",
                "var module$exports$mod$Bar = class Bar {",
                "  static fn() { use(this); }",
                "}"),
            lines(
                "/** @const */", //
                "var module$exports$client = {};",
                // preserve the 'this' value of 'class Bar {' - don't make this a free call.
                "module$exports$mod$Bar.fn();")));
  }

  @Test
  public void dontAddFreeCallIfModuleNotCallee() {
    test(
        srcs(
            lines(
                "goog.module('mod.Bar');", //
                "exports.NAME = 'BAR';"),
            lines(
                "goog.module('client');", //
                "const Bar = goog.require('mod.Bar');",
                "alert(Bar.NAME);")),
        expected(
            lines(
                "/** @const */",
                "var module$exports$mod$Bar = {};",
                "/** @const */",
                "module$exports$mod$Bar.NAME = 'BAR'"),
            lines(
                "/** @const */", //
                "var module$exports$client = {};",
                // don't mistake the Bar.NAME argument for the callee
                "alert(module$exports$mod$Bar.NAME);")));
  }

  @Test
  public void testTypeAndSourceInfoOfGoogRequireFromModule() {
    test(
        srcs(
            lines(
                "goog.module('mod.one');", //
                "exports.Bar = class {};"),
            lines(
                "goog.module('mod.two');", //
                "const {Bar} = goog.require('mod.one');",
                "new Bar();")),
        expected(
            lines(
                "/** @const */",
                "var module$exports$mod$one = {};",
                "/** @const */",
                "module$exports$mod$one.Bar = class {};"),
            lines(
                "/** @const */", //
                "var module$exports$mod$two = {};",
                "new module$exports$mod$one.Bar();")));

    // Verify the type of $module$exports$mod$one.Bar is correct
    Node secondScript = getLastCompiler().getJsRoot().getSecondChild();
    Node moduleExportsDotBar =
        NodeUtil.findPreorder(
            secondScript,
            (node) -> node.matchesQualifiedName("module$exports$mod$one.Bar"),
            Predicates.alwaysTrue());

    assertNode(moduleExportsDotBar).hasJSTypeThat().getReferenceNameIsEqualTo("mod.one.Bar");
    // The source info for the rewritten name must match the source info for `Bar` in `new Bar();`
    assertNode(moduleExportsDotBar)
        .hasSourceFileName(secondScript.getSourceFileName())
        .hasLineno(3)
        .hasCharno(4)
        .hasLength(3);
    assertNode(moduleExportsDotBar.getOnlyChild()).hasEqualSourceInfoTo(moduleExportsDotBar);
  }

  @Test
  public void testTypeOfGoogRequireFromLegacyModule() {
    test(
        srcs(
            lines(
                "goog.module('mod.one');", //
                "goog.module.declareLegacyNamespace();",
                "exports.Bar = class {};"),
            lines(
                "goog.module('mod.two');", //
                "const {Bar} = goog.require('mod.one');",
                "new Bar();")),
        expected(
            lines(
                "goog.provide('mod.one');", //
                "/** @const */",
                "mod.one.Bar = class {};"),
            lines(
                "/** @const */", //
                "var module$exports$mod$two = {};",
                "new mod.one.Bar();")));

    // Verify the type of mod.one.Bar is correct
    Node secondScript = getLastCompiler().getJsRoot().getSecondChild();
    Node moduleExportsDotBar =
        NodeUtil.findPreorder(
            secondScript,
            (node) -> node.matchesQualifiedName("mod.one.Bar"),
            Predicates.alwaysTrue());

    assertNode(moduleExportsDotBar).hasJSTypeThat().getReferenceNameIsEqualTo("mod.one.Bar");
  }

  @Test
  public void rewriteLendsAnnotation() {
    test(
        lines(
            "goog.module('mod');",
            "class Foo {",
            "  constructor() {",
            "    this.x = /** @lends {Foo.prototype} */ {};",
            "  }",
            "}"),
        lines(
            "/** @const */ var module$exports$mod = {};",
            "class module$contents$mod_Foo {",
            "  constructor() {",
            "    this.x = /** @lends {module$contents$mod_Foo.prototype} */  {};",
            "  }",
            "}"));
  }

  // This pass only handles goog.modules. ES6 modules are left alone.
  @Test
  public void testEs6Module() {
    testSame("export var x;");
    testSame("import {x} from '/y';");
  }

  @Test
  public void testDuplicateExportOfName() {
    ignoreWarnings(DiagnosticGroups.DUPLICATE_VARS);
    // ClosureCheckModule reports an error on this code, but that error is suppressed in some cases
    // so ClosureRewriteModule can't crash.
    test(
        lines("goog.module('Foo');", "class Foo {}", "exports = Foo;", "exports = Foo;"),
        lines(
            "class module$exports$Foo {",
            "}",
            "module$contents$Foo_Foo;",
            "/** @const */ var module$exports$Foo = module$contents$Foo_Foo;"));
  }

  @Test
  public void testDuplicateExportOfExpressionUsesLatter() {
    ignoreWarnings(DiagnosticGroups.DUPLICATE_VARS);
    // ClosureCheckModule reports an error on this code, but that error is suppressed in some cases
    // so ClosureRewriteModule can't crash.
    test(
        lines("goog.module('Foo');", "exports = 0;", "exports = 1;"),
        lines("0;", "/** @const */ var module$exports$Foo = 1;"));
  }

  @Test
  public void testTypeReferenceToDefaultExport() {
    test(
        lines(
            "goog.module('Foo');",
            "exports = class Bar {};",
            "/** @type {!exports} */",
            "const e = new exports();"),
        lines(
            "/** @const */",
            "var module$exports$Foo = class Bar {};",
            "/** @type {!module$exports$Foo} */",
            "const module$contents$Foo_e = new module$exports$Foo();"));
  }

  @Test
  public void testTypeReferenceToNamedExport() {
    test(
        lines(
            "goog.module('Foo');",
            "/** @enum {number} */",
            "exports.Foo = {A: 1};",
            "/** @type {!exports.Foo} */",
            "const z = exports.Foo.A;"),
        lines(
            "/** @const */",
            "var module$exports$Foo = {};",
            "/** @const @enum {number} */",
            "module$exports$Foo.Foo = {A: 1};",
            "/** @type {!module$exports$Foo.Foo} */",
            "const module$contents$Foo_z = module$exports$Foo.Foo.A;"));
  }
}
